# coding: utf-8
from itertools import chain

from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from mptt.managers import TreeManager
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
from taggit.models import Tag

from kpi.constants import PERM_VIEW_COLLECTION, PERM_CHANGE_COLLECTION, \
    PERM_DELETE_COLLECTION, PERM_SHARE_COLLECTION
from kpi.fields import KpiUidField
from .asset import (
    Asset,
    TaggableModelManager,
    KpiTaggableManager,
    TagStringMixin,
)
from .object_permission import ObjectPermission, ObjectPermissionMixin


class CollectionManager(TreeManager, TaggableModelManager):

    def create(self, *args, **kwargs):
        assets = False
        if 'assets' in kwargs:
            assets = kwargs['assets']
            del kwargs['assets']
        created = super().create(*args, **kwargs)
        if assets:
            new_assets = []
            for asset in assets:
                asset['parent'] = created
                new_assets.append(Asset.objects.create(**asset))
            # bulk_create comes with a number of caveats
            # Asset.objects.bulk_create(new_assets)
        return created

    def filter_by_tag_name(self, tag_name):
        try:
            tag = Tag.objects.get(name=tag_name)
        except Tag.DoesNotExist:
            return self.none()
        return self.filter(tags=tag)


class Collection(ObjectPermissionMixin, TagStringMixin, MPTTModel):
    name = models.CharField(max_length=255)
    parent = TreeForeignKey('self', null=True, blank=True,
                            related_name='children', on_delete=models.CASCADE)
    owner = models.ForeignKey('auth.User', related_name='owned_collections',
                              on_delete=models.CASCADE)
    editors_can_change_permissions = models.BooleanField(default=True)
    discoverable_when_public = models.BooleanField(default=False)
    uid = KpiUidField(uid_prefix='c')
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)
    objects = CollectionManager()
    tags = TaggableManager(manager=KpiTaggableManager)
    permissions = GenericRelation(ObjectPermission)

    @property
    def kind(self):
        return 'collection'

    class Meta:
        ordering = ('-date_modified',)
        permissions = (
            # change_, add_, and delete_collection are provided automatically
            # by Django
            (PERM_VIEW_COLLECTION, 'Can view collection'),
            (PERM_SHARE_COLLECTION,
             "Can change this collection's sharing settings"),
        )

        # Since Django 2.1, 4 permissions are added for each registered model:
        # - add
        # - change
        # - delete
        # - view
        # See https://docs.djangoproject.com/en/2.2/topics/auth/default/#default-permissions
        # for more detail.
        # `view_collection` clashes with newly built-in one.
        # The simplest way to fix this is to keep old behaviour
        default_permissions = ('add', 'change', 'delete')

    # Assignable permissions that are stored in the database
    ASSIGNABLE_PERMISSIONS = (PERM_VIEW_COLLECTION, PERM_CHANGE_COLLECTION)
    # Calculated permissions that are neither directly assignable nor stored
    # in the database, but instead implied by assignable permissions
    CALCULATED_PERMISSIONS = (PERM_SHARE_COLLECTION, PERM_DELETE_COLLECTION)
    # Granting some permissions implies also granting other permissions
    IMPLIED_PERMISSIONS = {
        # Format: explicit: (implied, implied, ...)
        PERM_CHANGE_COLLECTION: (PERM_VIEW_COLLECTION,),
    }

    def get_ancestors_or_none(self):
        # ancestors are ordered from farthest to nearest
        ancestors = self.get_ancestors()
        if ancestors.exists():
            return ancestors
        else:
            return None

    def get_mixed_children(self):
        """ Returns all children, both Assets and Collections """
        return CollectionChildrenQuerySet(self)

    def __str__(self):
        return self.name


class CollectionChildrenQuerySet:
    """ A pseudo-QuerySet containing mixed-model children of a collection.
    Collections are always listed before assets.  Derived from
    http://ramenlabs.com/2010/12/08/how-to-quack-like-a-queryset/.
    """
    def __init__(self, collection):
        self.collection = collection
        self.child_collections = collection.get_children()
        self.child_assets = collection.assets.all()

    def __iter__(self):
        for row in chain(self.child_collections, self.child_assets):
            yield row

    def __repr__(self):
        data = list(self[:models.query.REPR_OUTPUT_SIZE + 1])
        if len(data) > models.query.REPR_OUTPUT_SIZE:
            data[-1] = "...(remaining elements truncated)..."
        return repr(data)

    def __getitem__(self, k):
        if not isinstance(k, (slice, int)):
            raise TypeError
        assert ((not isinstance(k, slice) and (k >= 0))
                or (isinstance(k, slice) and (k.start is None or k.start >= 0)
                    and (k.stop is None or k.stop >= 0))), \
            "Negative indexing is not supported."

        qs = self._clone()
        collections = qs.child_collections
        assets = qs.child_assets

        if isinstance(k, slice):
            """ Colletions first, then Assets """
            collections_start = 0
            assets_start = 0
            collections_count = None

            if k.step is not None:
                raise NotImplementedError(
                    'Slicing with a step is not implemented.'
                )
            if k.start is not None:
                # It's alright if the slice starts beyond the end of the
                # QuerySet; Django will just return emptiness
                collections_start = k.start
                # Counting is expensive; store the result for future use
                collections_count = collections.count()
                assets_start = max(0, k.start - collections_count)
            if k.stop is None:
                qs.child_collections = collections[collections_start:]
                qs.child_assets = assets[assets_start:]
                return qs
            else:
                # It's alright of the slice ends beyond the end of the
                # QuerySet; Django will just stop at the last object
                collections_stop = k.stop
                if collections_count is None:
                    # Only count if we didn't already
                    collections_count = collections.count()
                assets_stop = max(0, k.stop - collections_count)
                qs.child_collections = collections[
                    collections_start:collections_stop]
                qs.child_assets = assets[assets_start:assets_stop]
                return qs
        else:
            try:
                return collections[k]
            except IndexError:
                return assets[k - collections.count()]

    def count(self):
        return self.child_collections.count() + self.child_assets.count()

    def all(self):
        return self._clone()

    def filter(self, *args, **kwargs):
        return self._apply_to_both('filter', *args, **kwargs)

    def exclude(self, *args, **kwargs):
        return self._apply_to_both('exclude', *args, **kwargs)

    def select_related(self, *args, **kwargs):
        return self._apply_to_both('select_related', *args, **kwargs)

    def prefetch_related(self, *args, **kwargs):
        return self._apply_to_both('prefetch_related', *args, **kwargs)

    def only(self, *args, **kwargs):
        return self._apply_to_both('only', *args, **kwargs)

    def defer(self, *args, **kwargs):
        return self._apply_to_both('defer', *args, **kwargs)

    def _apply_to_both(self, method, *args, **kwargs):
        qs = self._clone()
        qs.child_collections = getattr(qs.child_collections, method)(
            *args, **kwargs)
        qs.child_assets = getattr(qs.child_assets, method)(
            *args, **kwargs)
        return qs

    def _clone(self):
        qs = CollectionChildrenQuerySet(self.collection)
        qs.child_collections = self.child_collections._clone()
        qs.child_assets = self.child_assets._clone()
        return qs

    def optimize_for_list(self):
        def optimize_subquery_for_list(sub_qs):
            model = sub_qs.model
            try:
                optimize_method = model.optimize_queryset_for_list
            except AttributeError:
                return sub_qs
            else:
                return optimize_method(sub_qs)

        qs = self._clone()
        qs.child_collections = optimize_subquery_for_list(qs.child_collections)
        qs.child_assets = optimize_subquery_for_list(qs.child_assets)
        return qs


class UserCollectionSubscription(models.Model):
    """ Record a user's subscription to a publicly-discoverable collection,
    i.e. one that has `discoverable_when_public = True` """
    collection = models.ForeignKey(Collection, on_delete=models.CASCADE)
    user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    uid = KpiUidField(uid_prefix='b')

    class Meta:
        unique_together = ('collection', 'user')
